﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text.RegularExpressions;

﻿using OmniKits.Core;
using OmniKits.Extensions;

namespace OmniKits.ComponentModel
{
    [Obsolete("Contains ERRORS, DON'T USE!")]
    public class EnhancedEnumConverter : EnumConverter
    {
#pragma warning disable 1591

        private const string DefaultSeparatorString = ", ";
        private static readonly Regex DefaultSeparatorRegex = new Regex(@"\s*[,|]\s*", RegexOptions.CultureInvariant);

        public string Separator { get; private set; }
        public Regex ParsingSeparator { get; private set; }

        private volatile object _InitLock = new object();
        private Dictionary<string, ulong> _NameToUInt64 = new Dictionary<string, ulong>();
        private Dictionary<ulong, string> _UInt64ToDisplayNameDict;
        private Dictionary<ulong, string> _UInt64ToNameDict;
        private EnumElement[] _UInt64ToDisplayNameArray;
        private EnumElement[] _UInt64ToNameArray;
        struct EnumElement
        {
            public ulong Value;
            public string Name;
        }
        //private bool _ZeroIsZero;
        private bool _IsFlagEnum;
        private bool _IsUnsigned;

        private Func<ulong, object> _UInt64ToBoxed;
        private Func<object, ulong> _BoxedToUInt64;

        #region Constructors

        public EnhancedEnumConverter(Type type)
            : this(type, (string)null)
        { }

        public EnhancedEnumConverter(Type type, IEqualityComparer<string> comparer)
            : this(type, null, comparer)
        { }

        private static Regex SelectSeparatorRegex(string separator)
        {
            if (separator == null)
                return DefaultSeparatorRegex;
            return new Regex(@"\s*" + Regex.Escape(separator.Trim()) + @"\s*", RegexOptions.CultureInvariant);
        }
        public EnhancedEnumConverter(Type type, string separator, IEqualityComparer<string> comparer = null)
            : this(type, separator ?? DefaultSeparatorString, SelectSeparatorRegex(separator), comparer: comparer)
        { }

        public EnhancedEnumConverter(Type type, string separator, Regex parsingSeparator, IEqualityComparer<string> comparer = null)
            : base(type)
        {
            Separator = separator;
            ParsingSeparator = parsingSeparator;

            _NameToUInt64 = new Dictionary<string, ulong>(comparer);

            _IsFlagEnum = Attribute.IsDefined(type, typeof(FlagsAttribute));
            switch (Type.GetTypeCode(type.GetEnumUnderlyingType()))
            {
                case TypeCode.Byte:
                case TypeCode.UInt16:
                case TypeCode.UInt32:
                case TypeCode.UInt64:
                    _IsUnsigned = true;
                    break;
            }
            
            // can't do init here, calling virtual methods may fail due to derived .ctor is not executed yet
        }

        #endregion

        #region Inits

        struct FlagTypeBoxConverters
        {
            public Func<ulong, object> UInt64ToBoxed;
            public Func<object, ulong> BoxedToUInt64;
        }

        static FlagTypeBoxConverters MakeBoxConverters(Type type)
        {
            var underlyingType = type;
            if (type.IsEnum)
                underlyingType = type.GetEnumUnderlyingType();

            var dmUInt64ToBoxed = new DynamicMethod("UInt64ToBoxed", typeof(object), new[] { typeof(ulong) }, type);
            var ilUInt64ToBoxed = dmUInt64ToBoxed.GetILGenerator();
            ilUInt64ToBoxed.Emit(OpCodes.Ldarg_0);

            var dmBoxedToUInt64 = new DynamicMethod("BoxedToUInt64", typeof(ulong), new[] { typeof(object) }, type);
            var ilBoxedToUInt64 = dmBoxedToUInt64.GetILGenerator();
            ilBoxedToUInt64.Emit(OpCodes.Ldarg_0);
            ilBoxedToUInt64.Emit(OpCodes.Unbox_Any, type);
            //ilBoxedToUInt64.Emit(OpCodes.Unbox, type);
            //ilBoxedToUInt64.Emit(OpCodes.Ldobj, type);

            switch (Type.GetTypeCode(underlyingType))
            {
                #region Signed

                case TypeCode.SByte:
                    //ilUInt64ToBoxed.Emit(OpCodes.Conv_U1);
                    ilUInt64ToBoxed.Emit(OpCodes.Conv_I1);
                    break;
                case TypeCode.Int16:
                    //ilUInt64ToBoxed.Emit(OpCodes.Conv_U2);
                    ilUInt64ToBoxed.Emit(OpCodes.Conv_I2);
                    break;
                case TypeCode.Int32:
                    //ilUInt64ToBoxed.Emit(OpCodes.Conv_U4);
                    ilUInt64ToBoxed.Emit(OpCodes.Conv_I4);
                    break;
                case TypeCode.Int64:
                    //ilUInt64ToBoxed.Emit(OpCodes.Conv_U8);
                    ilUInt64ToBoxed.Emit(OpCodes.Conv_I8);
                    break;

                #endregion

                #region Unsigned

                case TypeCode.Byte:
                    ilUInt64ToBoxed.Emit(OpCodes.Conv_U1);
                    break;
                case TypeCode.UInt16:
                    ilUInt64ToBoxed.Emit(OpCodes.Conv_U2);
                    break;
                case TypeCode.UInt32:
                    ilUInt64ToBoxed.Emit(OpCodes.Conv_U4);
                    break;
                case TypeCode.UInt64:
                    // does nothing in dynamic methods
                    break;

                #endregion

                // wtf!?
                default:
                    //throw NewException.Argument(() => type);
                    return new FlagTypeBoxConverters();
            }

            ilUInt64ToBoxed.Emit(OpCodes.Box, type);
            ilUInt64ToBoxed.Emit(OpCodes.Ret);

            ilBoxedToUInt64.Emit(OpCodes.Conv_I8); // cam be omitted, leave here for clarify
            ilBoxedToUInt64.Emit(OpCodes.Conv_U8);
            ilBoxedToUInt64.Emit(OpCodes.Ret);

            return new FlagTypeBoxConverters
            {
                UInt64ToBoxed = (Func<ulong, object>)dmUInt64ToBoxed.CreateDelegate(typeof(Func<ulong, object>)),
                BoxedToUInt64 = (Func<object, ulong>)dmBoxedToUInt64.CreateDelegate(typeof(Func<object, ulong>)),
            };
        }

        private static readonly ConcurrentDictionary<Type, Lazy<FlagTypeBoxConverters>> BoxConverters
            = new ConcurrentDictionary<Type, Lazy<FlagTypeBoxConverters>>();
        static FlagTypeBoxConverters GetBoxConverters(Type type)
        {
            return BoxConverters.GetOrAdd(type, new Lazy<FlagTypeBoxConverters>(() => MakeBoxConverters(type))).Value;
        }

        private void EnsureMaps()
        {
            var initLock = _InitLock;
            if (initLock == null)
                return;
            lock (initLock)
            {
                if (_InitLock == null)
                    return;

                var converters = GetBoxConverters(EnumType);
                _UInt64ToBoxed = converters.UInt64ToBoxed;
                _BoxedToUInt64 = converters.BoxedToUInt64;
                //InitDynamicMethods(type);

                var fieldArr = EnumType.GetFields(BindingFlags.Public | BindingFlags.Static).Select(fi =>
                {
                    var boxed = fi.GetValue(null);
                    var value = _BoxedToUInt64(boxed);

                    return new
                    {
                        Name = fi.Name,
                        Value = value,
                        DisplayName = GetDisplayName(fi),
                    };
                }).ToArray();

                // build Name to Value map
                foreach (var el in fieldArr)
                {
                    _NameToUInt64[el.Name] = el.Value;

                    if (el.DisplayName != null)
                        _NameToUInt64[el.DisplayName] = el.Value;
                }

                // create array, for better performance
                var candidate = fieldArr.OrderBy(el => el.Value).ToArray();
                _UInt64ToNameArray = candidate.Select(e => new EnumElement { Name = e.Name, Value = e.Value }).ToArray();
                _UInt64ToDisplayNameArray = candidate.Select(e => new EnumElement { Name = e.DisplayName ?? e.Name, Value = e.Value }).ToArray();

                // if array is not the best, use dictionary instead
                if (candidate.Where((el, i) => el.Value != (ulong)i).Any())
                {
                    _UInt64ToNameDict = _UInt64ToNameArray.ToDictionary(el => el.Value, el => el.Name);
                    _UInt64ToDisplayNameDict = _UInt64ToDisplayNameArray.ToDictionary(el => el.Value, el => el.Name);
                }

                _InitLock = null;
            }
        }

        public static string GetFieldDisplayName(FieldInfo fieldInfo)
        {
            var dna = (DisplayNameAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DisplayNameAttribute));
            if (!(dna == null || dna.DisplayName.IsNullOrWhiteSpace()))
                return dna.DisplayName.Trim();
            else
                return null;
        }

        protected virtual string GetDisplayName(FieldInfo fieldInfo)
        {
            return GetFieldDisplayName(fieldInfo);
        }

        #endregion

        #region Overridings

        private object ConvertFromString(string str, CultureInfo culture)
        {
            EnsureMaps();

            ulong tmpULong;
            if (ulong.TryParse(str, NumberStyles.Integer, culture, out tmpULong))
                return _UInt64ToBoxed(tmpULong);

            long tmpLong;
            if (long.TryParse(str, NumberStyles.Integer, culture, out tmpLong))
                return _UInt64ToBoxed((ulong)tmpLong);

            ulong tmp = 0;
            if (_IsFlagEnum)
            {
                var strs = ParsingSeparator.Split(str).Where(s => !s.IsNullOrWhiteSpace());

                foreach (var s in strs)
                {
                    tmp |= _NameToUInt64[s];
                }
            }
            else
            {
                tmp = _NameToUInt64[str];
            }

            return _UInt64ToBoxed(tmp);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            var str = value as string;

            if (str == null)
                return base.ConvertFrom(context, culture, value);

            return ConvertFromString(str.Trim(), culture);
        }

        private string ConvertToString(ITypeDescriptorContext context, object value, CultureInfo culture)
        {
            EnsureMaps();

            var uint64ToNameArray = _UInt64ToDisplayNameArray;
            var uint64ToNameDict = _UInt64ToDisplayNameDict;

            if (!ShouldConvertToEnhancedString(context, value, culture))
            {
                uint64ToNameArray = _UInt64ToNameArray;
                uint64ToNameDict = _UInt64ToNameDict;
            }

            string result;
            var ulValue = _BoxedToUInt64(value);

            var i = uint64ToNameArray.Length;
            if (uint64ToNameDict != null)
            {
                if (uint64ToNameDict.TryGetValue(ulValue, out result))
                    return result;

                // if the value is 0 and 0 is not defined in the flags, then just return "0" instead of trying each of defined flags
                if (ulValue == 0)
                    return "0";
            }
            else
            {
                if (ulValue < (ulong)i)
                    return uint64ToNameArray[(int)ulValue].Name;
            }

            if (_IsFlagEnum)
            {
                var list = new List<EnumElement>(i);
                ulong flag;
                ulong test = 0;
                while (-1 < --i)
                {
                    flag = uint64ToNameArray[i].Value;
                    if (flag == 0)
                        break;

                    if ((ulValue & flag) == flag)
                    {
                        if ((test & flag) == flag)
                            continue;
                        test |= flag;

                        list.Add(uint64ToNameArray[i]);
                    }
                }

                if (test == ulValue)
                {
                    list.Reverse();
                    return string.Join(Separator, list.Select(el => el.Name).ToArray());
                }
            }

            if (_IsUnsigned)
                return ulValue.ToString();
            return ((long)ulValue).ToString();
        }

        protected virtual bool ShouldConvertToEnhancedString(ITypeDescriptorContext context, object value, CultureInfo culture)
        {
            return true;
        }

        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            if (destinationType != typeof(string))
                return base.ConvertTo(context, culture, value, destinationType);

            if (value == null)
                throw new ArgumentNullException(nameof(value));

            return ConvertToString(context, value, culture);
        }

        public override TypeConverter.StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
        {
            // TODO: think about improving
            return base.GetStandardValues(context);
        }

        public override bool IsValid(ITypeDescriptorContext context, object value)
        {
            if (value == null)
                return false;

            EnsureMaps();

            ulong tmp;
            var str = value as string;
            if (str != null)
            {
                return _NameToUInt64.TryGetValue(str, out tmp);
            }
            else
            {
                var type = value.GetType();
                if (type == EnumType)
                {
                    tmp = _BoxedToUInt64(value);
                }
                else if (type.IsEnum)
                {
                    throw new ArgumentException(null, nameof(value));
                }
                else
                {
                    var boxedToULong = GetBoxConverters(type).BoxedToUInt64;

                    if (boxedToULong == null)
                    {
                        // not really interested in the binary search sh!t
                        return base.IsValid(context, value);
                    }

                    tmp = boxedToULong(value);
                }

                if (_UInt64ToDisplayNameDict == null)
                {
                    return tmp < (ulong)_UInt64ToDisplayNameArray.Length;
                }
                else
                {
                    return _UInt64ToDisplayNameDict.TryGetValue(tmp, out str);
                }
            }
        }

        #endregion
    }
}